"""
Feature Matrix Diff Tool

 Inspects two feature matrices, comparing their keys and values, printing any
 discrepancies found between them. An optional translation table may be
 provided to unify features when their names are not equivalent.

 For implementation details, see below.

Author: Tom Robinson <trobinson@systemsbiology.org>
Initial Revision: February 17th, 2012

"""

## Imports

import logging
import sys

# Included:
from ftranslator import *
from kvparser import *


## Credit documentation

__author__ = "Tom Robinson"
__copyright__ = "Institute for Systems Biology, 2011"
__credits__ = ["Tom Robinson"]
__license__ = "Apache 2.0"
__version__ = "1.0"
__maintainer__ = "Tom Robinson"
__email__ = "trobinson@systemsbiology.org"
__status__ = "Production"
__platform__ = "Multi-OS, Python 2.6+"


## Constants

# Connector for compound feature names
CONNECTOR = ' '

# Epsilon table
EPSILON_16BIT = 4.88e-04
#EPSILON_32BIT = 5.96e-08
#EPSILON_64BIT = 1.11e-16
#EPSILON_128BIT = 9.63e-35

# Error codes
ERRCO_SUCCESS = 0               # Successful return
ERRCO_PRINTHELP = 1             # Help was printed, for some reason
ERRCO_UNDEFINED = 0xffffffff    # Undefined error with an expressive sentinel

# Separator used by input files
SEPARATOR = '\t'


## Globals
logger = logging.getLogger('fmParse')
handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

# Set the production log level
logger.setLevel(logging.ERROR)


## Functions

def echoHelp():
    """Echos help information to STDOUT
    This outputs CLI syntax and tool usage information. 

    Parameters
    ----------
    None

    Return Value
    ------------
    rcode : int
        ERRCO_PRINTHELP, always

    Examples
    --------
    echoHelp()

    """
    print \
    '''
    Perform a diff comparison of two feature matrices, with an optional
     translation table to unify feature names. 

     The translation table should be two columns separated by a tab character,
     the first describing the feature (by matching regular expression), the
     latter, the translation (replacement evaluated by the expression).

     For more information on what each should look like, please see:
      http://docs.python.org/library/re.html#re.sub

    Usage: %s <matrix A> <matrix B> [<translation table>]
     (eg. %s myFeatureMatrix.tsv myComparisonMatrix.tsv myTranslations.tsv)
    ''' % (sys.argv[0],sys.argv[0])

    return ERRCO_PRINTHELP


def formatValue(value):
    """ Coerce the given value into its most correct known type (from string)

    Parameters
    ----------
    value : string
        The given value for type conversion

    Return Value
    ------------
    object
        The original value, coerced as above

    Examples
    --------
    assert(isinstance(formatValue(1.0),float))

    """
    value = value.strip()
    if value.isdigit():
        return int(value)
    try:
        return float(value)
    except ValueError:
        return value
            

def getFeature(feature, translator):
    """ Extract and return the feature, in the context of the given feature
     translator; if no translator is provided, just return the feature itself

    Parameters
    ----------
    feature : string
        The given feature name for translation

    translator : FeatureTranslator
        The given FeatureTranslator instance; if None, no translation occurs

    Return Value
    ------------
    string
        Translated feature, after lookup (if applicable)

    Examples
    --------
    assert(getFeature(feature, None) == feature)

    """
    if translator:
        translations = translator.translations(feature)
        if translations:
            return re.sub(translations[0][0],translations[0][1],feature)
    
    return feature


def isEquivalent(left, right):
    """Compare two features to one another, by checking their type and
     performing equivalence testing based upon it.

    Parameters
    ----------
    left : object
        The left-hand object for comparison
    right : object
        The right-hand object for comparison

    Return Value
    ------------
    bool
        True if equivalent by this sole comparator; false otherwise.

    Examples
    --------
    assert(isEquivalent(1,1) == True)

    """ 
    # If the classes are not equivalent, fail immediately.
    if not isinstance(left, right.__class__):
        return False

    if isinstance(left,float):
        # We tolerate floating point roundoff to a single 16 bit
        #  operation. Anything less precise will fail.
        return abs(left - right) < EPSILON_16BIT

    else:
        # For classes that expose __cmp__ or __eq__ correctly, as with all
        #  language primitives and any class where this behavior is
        #  desired. This will fail for default subclasses of object, unless
        #  left and right point to the same object in memory. 
        #
        #  For reference:
        #
        #   # Two separate objects are not memory equivalent...
        #   >>> object() == object()
        #   False
        #
        #   # ... but the same object reference, is.
        #   >>> a = object()
        #   >>> a == a
        #   True
        return left == right


def _fmain():
    """Function main: analog for the "main" function in other languages

     This is non-standard, and should not be confused with __main__, 
     the (otherwise anonymous) scope in which the Python interpreter's main
     program executes. We call this function through a check for it, though.
    
     If the right number of arguments are given to sys.argv, perform
     operation. Otherwise, echo usage text and exit.

    Parameters
    ----------
    None (arguments supplied by sys.argv)

    Return Value
    ------------
    int
        A CLI-conformant return code 
        0 for success; enumerated failure code otherwise 

    Examples
    --------
    assert(_fmain() == 0)

    """ 

    # If our sanity check fails, exit
    if len(sys.argv) != 3 and len(sys.argv) != 4:
        return echoHelp()

    # Create the variables we're interested in using for processing
    leftMatrixFname = None
    rightMatrixFname = None
    featureTranslator = None

    # Propagate them based on the length of sys.argv
    leftMatrixFname = sys.argv[1]
    rightMatrixFname = sys.argv[2]
    if len(sys.argv) == 4:
        featureTranslator = FeatureTranslator(logger)
        featureTranslator.appendFromFile(sys.argv[3])

    # Open and read each of our files into two respective dictionaries,
    #  performing name unification as we extract each feature name
    left = {}
    right = {}

    for rst in ColumnarKVGenerator( \
      open(leftMatrixFname,'rU'), \
      keycols=[0], \
      additionalSkipLines=[0,0], \
      connector=CONNECTOR, \
      separator=SEPARATOR):

        logger.info("Now parsing: (left matrix) %s" % rst[0])
        left[getFeature(rst[0],featureTranslator)] = formatValue(rst[1])

    for rst in ColumnarKVGenerator( \
      open(rightMatrixFname,'rU'), \
      keycols=[0], \
      additionalSkipLines=[0,0], \
      connector=CONNECTOR, \
      separator=SEPARATOR):

        logger.info("Now parsing: (right matrix) %s" % rst[0])
        right[getFeature(rst[0],featureTranslator)] = formatValue(rst[1])

    # Perform the comparison, starting with our left matrix
    logger.info("Beginning diff...")
    for item in left.items():
        if item[0] in right:
            rv = right.pop(item[0])
            if not isEquivalent(item[1],rv):
                print "%s differed: %s versus %s!" % (item[0],item[1],rv)
        else:
            print "%s was not in right matrix!" % item[0]

    # Now, walk our right matrix, printing off all the keys that remain
    for key in right.keys():
        print "%s was not in left matrix!" % key

    # And, done.
    logger.info("Done!")

    return ERRCO_SUCCESS
    

# Initialization and Execution
#  Direct invocation
if __name__ == "__main__":
    exit(_fmain())

# Library invocation
#  If included as part of a package, the caller is responsible for
#  initialization, setup, and any additional operations.
#
#  (For example, executing _fmain())
